<script lang="tsx">
import type { CSSProperties } from 'vue'
import { computed, defineComponent, nextTick, onMounted, reactive, ref, unref, watch } from 'vue'
import { useEventListener } from '@/hooks/event/useEventListener'
import { getSlot } from '@/utils/helper/tsxHelper'

type NumberOrNumberString = PropType<string | number | undefined>

const props = {
  height: [Number, String] as NumberOrNumberString,
  maxHeight: [Number, String] as NumberOrNumberString,
  maxWidth: [Number, String] as NumberOrNumberString,
  minHeight: [Number, String] as NumberOrNumberString,
  minWidth: [Number, String] as NumberOrNumberString,
  width: [Number, String] as NumberOrNumberString,
  bench: {
    type: [Number, String] as NumberOrNumberString,
    default: 0,
  },
  itemHeight: {
    type: [Number, String] as NumberOrNumberString,
    required: true,
  },
  items: {
    type: Array as PropType<any[]>,
    default: () => [],
  },
}

const prefixCls = 'virtual-scroll'

function convertToUnit(str: string | number | null | undefined, unit = 'px'): string | undefined {
  if (str == null || str === '')
    return undefined
  else if (Number.isNaN(+str!))
    return String(str)
  else
    return `${Number(str)}${unit}`
}

export default defineComponent({
  name: 'VirtualScroll',
  props,
  setup(props, { slots }) {
    const wrapElRef = ref<HTMLDivElement | null>(null)
    const state = reactive({
      first: 0,
      last: 0,
      scrollTop: 0,
    })

    const getBenchRef = computed(() => {
      return Number.parseInt(props.bench as string, 10)
    })

    const getItemHeightRef = computed(() => {
      return Number.parseInt(props.itemHeight as string, 10)
    })

    const getFirstToRenderRef = computed(() => {
      return Math.max(0, state.first - unref(getBenchRef))
    })

    const getLastToRenderRef = computed(() => {
      return Math.min((props.items || []).length, state.last + unref(getBenchRef))
    })

    const getContainerStyleRef = computed((): CSSProperties => {
      return {
        height: convertToUnit((props.items || []).length * unref(getItemHeightRef)),
      }
    })

    const getWrapStyleRef = computed((): CSSProperties => {
      const styles: Recordable<string> = {}
      const height = convertToUnit(props.height)
      const minHeight = convertToUnit(props.minHeight)
      const minWidth = convertToUnit(props.minWidth)
      const maxHeight = convertToUnit(props.maxHeight)
      const maxWidth = convertToUnit(props.maxWidth)
      const width = convertToUnit(props.width)

      if (height)
        styles.height = height
      if (minHeight)
        styles.minHeight = minHeight
      if (minWidth)
        styles.minWidth = minWidth
      if (maxHeight)
        styles.maxHeight = maxHeight
      if (maxWidth)
        styles.maxWidth = maxWidth
      if (width)
        styles.width = width
      return styles
    })

    watch([() => props.itemHeight, () => props.height], () => {
      onScroll()
    })

    function getLast(first: number): number {
      const wrapEl = unref(wrapElRef)
      if (!wrapEl)
        return 0

      const height = Number.parseInt(`${props.height || 0}`, 10) || wrapEl.clientHeight

      return first + Math.ceil(height / unref(getItemHeightRef))
    }

    function getFirst(): number {
      return Math.floor(state.scrollTop / unref(getItemHeightRef))
    }

    function onScroll() {
      const wrapEl = unref(wrapElRef)
      if (!wrapEl)
        return

      state.scrollTop = wrapEl.scrollTop
      state.first = getFirst()
      state.last = getLast(state.first)
    }

    function renderChildren() {
      const { items = [] } = props
      return items.slice(unref(getFirstToRenderRef), unref(getLastToRenderRef)).map(genChild)
    }

    function genChild(item: any, index: number) {
      index += unref(getFirstToRenderRef)
      const top = convertToUnit(index * unref(getItemHeightRef))
      return (
        <div class={`${prefixCls}__item`} style={{ top }} key={index}>
          {getSlot(slots, 'default', { index, item })}
        </div>
      )
    }

    onMounted(() => {
      state.last = getLast(0)
      nextTick(() => {
        const wrapEl = unref(wrapElRef)
        if (!wrapEl)
          return

        useEventListener({
          el: wrapEl,
          name: 'scroll',
          listener: onScroll,
          wait: 0,
        })
      })
    })

    return () => (
      <div class={prefixCls} style={unref(getWrapStyleRef)} ref={wrapElRef}>
        <div class={`${prefixCls}__container`} style={unref(getContainerStyleRef)}>
          {renderChildren()}
        </div>
      </div>
    )
  },
})
</script>

<style scoped lang="less">
.virtual-scroll {
  position: relative;
  display: block;
  flex: 1 1 auto;
  width: 100%;
  max-width: 100%;
  overflow: auto;

  &__container {
    display: block;
  }

  &__item {
    position: absolute;
    right: 0;
    left: 0;
  }
}
</style>
